/**
 * esriJson 与 geoJson的转换类
 * @module EsriJSONUtils
 */
type PointType = number[]
type CoordinatesType = number[][]
/**
 * 检查两个点是否相等
 * @static
 * @param {PointType} ponitA - 点a
 * @param {PointType} ponitB - 点b
 * @returns {Boolean}
 */
const pointsEqual = (a: PointType, b: PointType) => {
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      return false
    }
  }
  return true
}

/**
 * 闭合环，先判断环的第一个坐标点和最后一个坐标点是否相等，相等则已经闭合,否则把第一个坐标点复制一份到最后一个坐标点
 * @static
 * @param {Array} coordinates - 坐标数组
 * @returns {Array} 闭合的坐标数组
 */
const closeRing = (coordinates: any) => {
  if (!pointsEqual(coordinates[0], coordinates[coordinates.length - 1])) {
    coordinates.push(coordinates[0])
  }
  return coordinates
}

/**
 * 检查多边形环坐标是否为顺时针方向,顺时针表示外环，逆时针表示内环
 * http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order
 * @static
 * @param {CoordinatesType} ringToTest - 坐标数组
 * @returns {Boolean} - true:顺时针（外环）   false:逆时针(内环)
 */
const ringIsClockwise = (ringToTest: CoordinatesType) => {
  let total = 0
  let i = 0
  const rLength = ringToTest.length
  let pt1 = ringToTest[i]
  let pt2
  for (i; i < rLength - 1; i++) {
    pt2 = ringToTest[i + 1]
    total += (pt2[0] - pt1[0]) * (pt2[1] + pt1[1])
    pt1 = pt2
  }
  return total >= 0
}

/**
 * 判断顶点是否有相交
 * @static
 * @param {PointType} a1 - 坐标点a1
 * @param {PointType} a2 - 坐标点a2
 * @param {PointType} b1 - 坐标点b1
 * @param {PointType} b2 - 坐标点b2
 * @returns {Boolean}
 */
const vertexIntersectsVertex = (
  a1: PointType,
  a2: PointType,
  b1: PointType,
  b2: PointType
) => {
  const uaT =
    (b2[0] - b1[0]) * (a1[1] - b1[1]) - (b2[1] - b1[1]) * (a1[0] - b1[0])
  const ubT =
    (a2[0] - a1[0]) * (a1[1] - b1[1]) - (a2[1] - a1[1]) * (a1[0] - b1[0])
  const uB =
    (b2[1] - b1[1]) * (a2[0] - a1[0]) - (b2[0] - b1[0]) * (a2[1] - a1[1])

  if (uB !== 0) {
    const ua = uaT / uB
    const ub = ubT / uB

    if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
      return true
    }
  }

  return false
}

/**
 * 判断两个坐标数组是否有相交
 * @static
 * @param {CoordinatesType} a - 坐标数组a
 * @param {CoordinatesType} b - 坐标数组b
 * @returns {Boolean}
 */
const arrayIntersectsArray = (a: CoordinatesType, b: CoordinatesType) => {
  for (let i = 0; i < a.length - 1; i++) {
    for (let j = 0; j < b.length - 1; j++) {
      if (vertexIntersectsVertex(a[i], a[i + 1], b[j], b[j + 1])) {
        return true
      }
    }
  }

  return false
}

/**
 * 判断坐标数组是否包含某个坐标点
 * @static
 * @param {CoordinatesType} coordinates - 坐标数组
 * @param {PointType} point - 坐标点
 * @returns {Boolean}
 */
const coordinatesContainPoint = (
  coordinates: CoordinatesType,
  point: PointType
) => {
  let contains = false
  for (let i = -1, l = coordinates.length, j = l - 1; ++i < l; j = i) {
    if (
      ((coordinates[i][1] <= point[1] && point[1] < coordinates[j][1]) ||
        (coordinates[j][1] <= point[1] && point[1] < coordinates[i][1])) &&
      point[0] <
        ((coordinates[j][0] - coordinates[i][0]) *
          (point[1] - coordinates[i][1])) /
          (coordinates[j][1] - coordinates[i][1]) +
          coordinates[i][0]
    ) {
      contains = !contains
    }
  }
  return contains
}

/**
 * 判断坐标数组是否包含另一个坐标数组
 * @static
 * @param {CoordinatesType} outer - 外层坐标数组
 * @param {CoordinatesType} inner - 内层坐标数组
 * @returns {Boolean}
 */
const coordinatesContainCoordinates = (
  outer: CoordinatesType,
  inner: CoordinatesType
) => {
  const intersects = arrayIntersectsArray(outer, inner)
  const contains = coordinatesContainPoint(outer, inner[0])
  if (!intersects && contains) {
    return true
  }
  return false
}

/**
 * 判断坐标数组中的所有多边形是否有包含坐标数组中的某个多边形,用于检查Esri rings中的内环
 * @static
 * @param {CoordinatesType} rings - 坐标数组
 * @returns {Object}
 */
const convertRingsToGeoJSON = (rings: CoordinatesType) => {
  const outerRings: any[] = [] // 外环
  const holes: any[] = [] // 内环
  let x // 迭代变量
  let outerRing
  let hole

  // 遍历rings
  for (let r = 0; r < rings.length; r++) {
    const ring = closeRing(rings[r].slice(0))
    if (ring.length < 4) {
      continue
    }
    // 判断是否顺时针
    if (ringIsClockwise(ring)) {
      // 当前是顺时针（EsriJSON的外环）
      const polygon = [ring.slice().reverse()] // 转换为逆时针（GeoJSON的外环）
      outerRings.push(polygon)
    } else {
      // 当前是逆时针（EsriJSON的内环）
      holes.push(ring.slice().reverse()) // 转为顺时针（GeoJSON的内环）
    }
  }

  const uncontainedHoles: any[] = []

  // 当有内环时
  while (holes.length) {
    hole = holes.pop() // 取出内环数组的最后一个元素

    // 把所有的外环多边形遍历一遍，检查是否包含当前这个内环
    let contained = false
    for (x = outerRings.length - 1; x >= 0; x--) {
      outerRing = outerRings[x][0]
      if (coordinatesContainCoordinates(outerRing, hole)) {
        // 如果有外环多边形包含这个内环，将它添加到当前这个外环多边形的数组中
        outerRings[x].push(hole)
        contained = true
        break
      }
    }

    // 如果当前内环不在任何一个外环中，先添加到uncontainedHoles-未匹配数组里进一步做相交处理
    // https://github.com/Esri/esri-leaflet/issues/320
    if (!contained) {
      uncontainedHoles.push(hole)
    }
  }

  // 当存在内环没有匹配到任何外环里时，尝试使用相交来判断
  while (uncontainedHoles.length) {
    hole = uncontainedHoles.pop() // 取出未匹配的内环数组中最后一个元素

    // 继续把所有的外环多边形遍历一遍，检查是否有和这个内环存在相交
    let intersects = false
    for (x = outerRings.length - 1; x >= 0; x--) {
      outerRing = outerRings[x][0]
      if (arrayIntersectsArray(outerRing, hole)) {
        // 如果有外环多边形和当前内环相交，将它添加到当前这个外环多边形的数组中
        outerRings[x].push(hole)
        intersects = true
        break
      }
    }

    if (!intersects) {
      outerRings.push([hole.reverse()])
    }
  }

  if (outerRings.length === 1) {
    return {
      type: 'Polygon',
      coordinates: outerRings[0]
    }
  } else {
    return {
      type: 'MultiPolygon',
      coordinates: outerRings
    }
  }
}

/**
 * 确保环的方向正确，外环为顺时针方向，内环为逆时针方向 （GeoJSON -> EsriJSON）
 * @static
 * @param {CoordinatesType} poly - 坐标数组
 * @returns {Array} - 方向正确的坐标数组
 */
const orientRings = (poly: any) => {
  const output: any[] = []
  const polygon: any = poly.slice(0)
  const outerRing = closeRing(polygon.shift().slice(0))
  if (outerRing.length >= 4) {
    if (!ringIsClockwise(outerRing)) {
      outerRing.reverse()
    }

    output.push(outerRing)

    for (let i = 0; i < polygon.length; i++) {
      const hole = closeRing(polygon[i].slice(0))
      if (hole.length >= 4) {
        if (ringIsClockwise(hole)) {
          hole.reverse()
        }
        output.push(hole)
      }
    }
  }

  return output
}

/**
 * 将MultiPolygons转为单个Polygon Array （GeoJSON -> EsriJSON）
 *  [
 *    [
 *      [ array of outer coordinates ]
 *      [ hole coordinates ]
 *      [ hole coordinates ]
 *    ],
 *    [
 *      [ array of outer coordinates ]
 *      [ hole coordinates ]
 *      [ hole coordinates ]
 *    ],
 *  ]
 *  转换为
 *  [
 *    [ array of outer coordinates ]
 *    [ hole coordinates ]
 *    [ hole coordinates ]
 *    [ array of outer coordinates ]
 *    [ hole coordinates ]
 *    [ hole coordinates ]
 *  ]
 * @static
 * @param {Array} rings - 坐标数组
 * @returns {Array} - 转换后的坐标数组
 */
const flattenMultiPolygonRings = (rings: any) => {
  const output: any[] = []
  for (let i = 0; i < rings.length; i++) {
    const polygon: any = orientRings(rings[i])
    for (let x = polygon.length - 1; x >= 0; x--) {
      const ring = polygon[x].slice(0)
      output.push(ring)
    }
  }
  return output
}

/**
 * 对象浅拷贝
 * @static
 * @param {Object} obj - 原始对象
 * @returns {Object} - 浅拷贝后的新对象
 */
const shallowClone = (obj: any) => {
  const target: any = {}
  for (const i in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, i)) {
      target[i] = obj[i]
    }
  }
  return target
}

/**
 * 获取属性里的ID值
 * @static
 * @param {Object} attributes - 原始对象
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {String|Number} ID值
 */
const getId = (attributes: any, idAttribute?: string | number) => {
  const keys = idAttribute
    ? [idAttribute, 'OBJECTID', 'FID']
    : ['OBJECTID', 'FID']
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    if (
      key in attributes &&
      (typeof attributes[key] === 'string' ||
        typeof attributes[key] === 'number')
    ) {
      return attributes[key]
    }
  }
  throw new Error('No valid id attribute found')
}

// 可能匹配的ID正则
const knownFieldNames = /^(OBJECTID|FID|OID|ID)$/i

/**
 * 从Feature中获取ID属性
 * @static
 * @param {Object} feature - Feature对象
 * @returns {String} ID属性值
 */
const _findIdAttributeFromFeature = (feature: any) => {
  for (const key in feature.attributes) {
    if (key.match(knownFieldNames)) {
      return key
    }
  }
}

/**
 * 从FeatureSets中获取ID属性值
 * @static
 * @param {Object} feature - FeatureSets对象
 * @returns {String} ID属性值
 */
const _findIdAttributeFromFeatureSets = (featureSets: any) => {
  let result
  if (featureSets.fields) {
    // 1、如果没有objectIdFieldName，且存在fields属性，则在这里面寻找ID字段
    for (let j = 0; j <= featureSets.fields.length - 1; j++) {
      // 2、优先检测是否存在esriFieldTypeOID字段
      if (featureSets.fields[j].name === 'esriFieldTypeOID') {
        result = featureSets.fields[j].name
        break
      }
    }
    if (!result) {
      // 3、如果没有esriFieldTypeOID，尝试匹配包含OBJECTID|FID|OID|ID的字段
      for (let j = 0; j <= featureSets.fields.length - 1; j++) {
        if (featureSets.fields[j].name.match(knownFieldNames)) {
          result = featureSets.fields[j].name
          break
        }
      }
    }
  }
  return result
}

/**
 * 将GeoJSON类型转换为EsriJSON Geometry类型
 * @static
 * @param {Object} geojson - GeoJSON
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {Object|Array} EsriJSON Geometry
 */
const geoJSONToEsriJSON = (geojson: any, idAttribute?: string | number) => {
  idAttribute = idAttribute || 'OBJECTID'
  let spatialReference  
  if (geojson?.crs) {
    if (geojson.crs.properties.isWKID) {
      spatialReference = { wkid: geojson.crs.properties.name }
    } else {
      const wkt = geojson.crs.properties?.name || ''
      const wkid = wkt.toUpperCase().replace('EPSG:', '')
      spatialReference = { wkid }
    }
  }
  let result: any = {}
  switch (geojson.type) {
    case 'Point':
      result.x = geojson.coordinates[0]
      result.y = geojson.coordinates[1]
      spatialReference && (result.spatialReference = spatialReference)
      break
    case 'MultiPoint':
      result.points = geojson.coordinates.slice(0)
      spatialReference && (result.spatialReference = spatialReference)
      break
    case 'LineString':
      result.paths = [geojson.coordinates.slice(0)]
      spatialReference && (result.spatialReference = spatialReference)
      break
    case 'MultiLineString':
      result.paths = geojson.coordinates.slice(0)
      spatialReference && (result.spatialReference = spatialReference)
      break
    case 'Polygon':
      result.rings = orientRings(geojson.coordinates.slice(0))
      spatialReference && (result.spatialReference = spatialReference)
      break
    case 'MultiPolygon':
      result.rings = flattenMultiPolygonRings(geojson.coordinates.slice(0))
      spatialReference && (result.spatialReference = spatialReference)
      break
    case 'Feature':
      if (geojson.geometry) {
        result.geometry = geoJSONToEsriJSON(geojson.geometry, idAttribute)
      }
      result.attributes = geojson.properties
        ? shallowClone(geojson.properties)
        : {}
      if (geojson.id) {
        result.attributes[idAttribute] = geojson.id
      }
      break
    case 'FeatureCollection':
      result = []
      for (let i = 0; i < geojson.features.length; i++) {
        const feature = geojson.features[i]
        result.push(geoJSONToEsriJSON(feature, idAttribute))
      }
      break
    case 'GeometryCollection':
      result = []
      for (let i = 0; i < geojson.geometries.length; i++) {
        const geometry = geojson.geometries[i]
        result.push(geoJSONToEsriJSON(geometry, idAttribute))
      }
      break
  }

  return result
}

/**
 * 单个GeoJSON要素对象集合(GeoJSON FeatureCollection)转Esri要素对象集合(EsriJSON FeatureSet)
 * @static
 * @param {Object} featureCollection 单个GeoJSON要素对象集合（单个featureCollection对象）
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {Array|Object} EsriJSON FeatureSet
 */
const featureCollectionToFeatureSet = (
  featureCollection: any,
  idAttribute?: string | number
) => {
  const geoJsonTemps: any = []
  if (featureCollection.type === 'FeatureCollection') {
    if (featureCollection.features) {
      featureCollection.features.forEach((geojsonFeature: any) => {
        let tmp: any = null
        // MultiPolygon、Polygon 对应 Esri 都是esriGeometryPolygon
        for (let i = 0; i < geoJsonTemps.length; i++) {
          const g = geoJsonTemps[i]
          if (
            geojsonFeature.geometry.type.includes('Polygon') &&
            g.key.includes('Polygon')
          ) {
            tmp = g
            break
          }
        }
        if (tmp) {
          tmp.features.push(geojsonFeature)
        } else {
          geoJsonTemps.push({
            type: 'FeatureCollection',
            features: [geojsonFeature],
            key: geojsonFeature.geometry.type
          })
        }
      })
    }
  }
  const result: any = []
  geoJsonTemps.forEach((tmp: any) => {
    const featureset: any = {}
    switch (tmp.key) {
      case 'Point':
        featureset.geometryType = 'esriGeometryPoint'
        break
      case 'MultiPoint':
        featureset.geometryType = 'esriGeometryMultipoint'
        break
      case 'LineString':
        featureset.geometryType = 'esriGeometryPolyline'
        break
      case 'MultiLineString':
        featureset.geometryType = 'esriGeometryPolyline'
        break
      case 'Polygon':
        featureset.geometryType = 'esriGeometryPolygon'
        break
      case 'MultiPolygon':
        featureset.geometryType = 'esriGeometryPolygon'
        break
    }
    featureset.features = geoJSONToEsriJSON(tmp, idAttribute)
    result.push(featureset)
  })
  if (result.length && result.length === 1) {
    return result[0]
  }
  return result
}

/**
 * 多个GeoJSON要素对象集合转Esri要素对象集合
 * @static
 * @param {Array} featureCollections 多个GeoJSON要素对象集合（多个featureCollection对象组成的数组）
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {Array|Object} FeatureSets
 */
const featureCollectionsToFeatureSets = (
  featureCollections: any,
  idAttribute?: string | number
) => {
  if (featureCollections) {
    if (Array.isArray(featureCollections)) {
      const result: any[] = []
      for (let i = 0; i < featureCollections.length; i++) {
        result.push(
          featureCollectionToFeatureSet(featureCollections[i], idAttribute)
        )
      }
      return result
    } else {
      return featureCollectionToFeatureSet(featureCollections, idAttribute)
    }
  }
  return null
}

/**
 * 将EsriJSON Geometry类型转换为GeoJSON类型
 * @static
 * @param {Object} esriJson - EsriJSON Geometry
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {Object} GeoJSON
 */
const esriJSONToGeoJSON = (esriJson: any, idAttribute?: string | number) => {
  let geojson: any = {}

  if (typeof esriJson.x === 'number' && typeof esriJson.y === 'number') {
    geojson.type = 'Point'
    geojson.coordinates = [esriJson.x, esriJson.y]
    if (typeof esriJson.z === 'number') {
      geojson.coordinates.push(esriJson.z)
    }
  }

  if (esriJson.points) {
    geojson.type = 'MultiPoint'
    geojson.coordinates = esriJson.points.slice(0)
  }

  if (esriJson.paths) {
    if (esriJson.paths.length === 1) {
      geojson.type = 'LineString'
      geojson.coordinates = esriJson.paths[0].slice(0)
    } else {
      geojson.type = 'MultiLineString'
      geojson.coordinates = esriJson.paths.slice(0)
    }
  }

  if (esriJson.rings) {
    geojson = convertRingsToGeoJSON(esriJson.rings.slice(0))
  }

  if (esriJson.geometry || esriJson.attributes) {
    geojson.type = 'Feature'
    geojson.geometry = esriJson.geometry
      ? esriJSONToGeoJSON(esriJson.geometry)
      : null
    geojson.properties = esriJson.attributes
      ? shallowClone(esriJson.attributes)
      : null
    if (esriJson.attributes) {
      try {
        geojson.id = getId(esriJson.attributes, idAttribute)
      } catch (err) {
        // 无法设置ID
      }
    }
  }

  if (JSON.stringify(geojson.geometry) === JSON.stringify({})) {
    geojson.geometry = null
  }

  if (esriJson.spatialReference && esriJson.spatialReference.wkid) {
    geojson.crs = {
      type: 'name',
      properties: {
        name: esriJson.spatialReference.wkid,
        isWKID: true
      }
    }
  }
  return geojson
}

/**
 * 单个EsriJSON要素对象(featureSet)转GeoJSON要素对象集合 (featureCollection)
 * @param {Object} featureSet 单个Esri要素对象集合（featureSet对象）
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {{type: string, features: Array}} GeoJSON要素对象集合(FeatureCollection)
 */
const featureSetToFeatureCollection = (
  featureSet: any,
  idAttribute?: string | number
) => {
  let objectIdField
  const features = featureSet.features
  const spr = featureSet.spatialReference
  if (features && features.length && spr) {
    // 赋空间参考
    features.forEach((f: any) => {
      f.geometry = f.geometry || {}
      f.geometry.spatialReference = f.geometry.spatialReference || spr
    })
  }
  const count = features.length

  if (idAttribute) {
    objectIdField = idAttribute
  } else {
    objectIdField = _findIdAttributeFromFeatureSets(featureSet)
  }
  const featureCollection: any = {
    type: 'FeatureCollection',
    features: []
  }

  if (count) {
    for (let i = features.length - 1; i >= 0; i--) {
      const feature = esriJSONToGeoJSON(
        features[i],
        objectIdField || _findIdAttributeFromFeature(features[i])
      )
      featureCollection.features.push(feature)
    }
  }
  featureSet.fieldAliases &&
    (featureCollection.fieldAliases = featureSet.fieldAliases)
  featureSet.fields && (featureCollection.fields = featureSet.fields)
  return featureCollection
}

/**
 * 多个Esri要素对象集合转GeoJSON要素对象集合
 * @param {Array} featureSets 多个Esri要素对象集合（多个featureSet对象组成的数组）
 * @param {String|Number} [idAttribute] - ID的属性值
 * @returns {{type: string, features: Array}} GeoJSON要素对象集合
 */
const featureSetsToFeatureCollections = (
  featureSets: any,
  idAttribute?: string | number
) => {
  if (featureSets) {
    if (Array.isArray(featureSets)) {
      const result: any[] = []
      for (let i = 0; i < featureSets.length; i++) {
        result.push(featureSetToFeatureCollection(featureSets[i], idAttribute))
      }
      return result
    } else {
      return featureSetToFeatureCollection(featureSets, idAttribute)
    }
  }
  return null
}

export const EsriJSONUtils = {
  geoJSONToEsriJSON,
  featureCollectionToFeatureSet,
  featureCollectionsToFeatureSets,
  esriJSONToGeoJSON,
  featureSetToFeatureCollection,
  featureSetsToFeatureCollections
}
export default EsriJSONUtils
